package jblip.util.json;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;

public class JSONParser {

  public JSONValue<?> parseStream(InputStream input) {
    PushbackReader reader = new PushbackReader(new InputStreamReader(input));
    try {
      return scanValue(reader);
    } catch (IOException e) {
      e.printStackTrace();
      return null;
    } catch (JSONException e) {
      e.printStackTrace();
      return null;
    }
  }

  private JSONValue<?> scanValue(PushbackReader reader) throws IOException,
      JSONException {
    char next = scanCharacter(reader);

    switch (next) {
      case '{':
        return scanObject(reader);
      case '[':
        return scanArray(reader);
      case '"':
        return scanString(reader);
      default:
        reader.unread(next);
        if (next == '-' || (next >= '0' && next <= '9')) {
          return scanNumber(reader);
        } else {
          return scanBooleanOrNull(reader);
        }
    }
  }

  private JSONArray scanArray(PushbackReader reader) throws IOException,
      JSONException {
    JSONArray array = new JSONArray();
    boolean commavalid = false;

    while (true) {
      char c = scanCharacter(reader);

      switch (c) {
        case ']':
          return array;
        case ',':
          if (!commavalid) {
            throw new JSONException("Comma not expected here");
          } else {
            array.add(scanValue(reader));
            break;
          }
        default:
          reader.unread(c);
          array.add(scanValue(reader));
          commavalid = true;
      }
    }
  }

  private JSONValue<?> scanBooleanOrNull(PushbackReader reader)
      throws JSONException, IOException {
    StringBuilder builder = new StringBuilder();
    builder.append(scanCharacter(reader));

    char next;
    do {
      next = (char) reader.read();
      builder.append(next);
    } while (Character.isLowerCase(next));
    reader.unread(next);
    builder.deleteCharAt(builder.length() - 1);

    String result = builder.toString();
    if (result.equals("null")) {
      return JSONNull.INSTANCE;
    } else if (result.equals("true") || result.equals("false")) {
      return new JSONBoolean(Boolean.getBoolean(result));
    } else {
      throw new JSONException("Unknown identifier: " + result);
    }
  }

  private JSONNumber scanNumber(PushbackReader reader) throws IOException,
      JSONException {
    StringBuilder builder = new StringBuilder();
    builder.append(scanCharacter(reader));
    int next;
    do {
      next = reader.read();
      if (next == -1) {
        break;
      }
      builder.append((char) next);
    } while ((next >= '0' && next <= '9') || next == 'e' || next == 'E' ||
        next == '.' || next == '-' || next == '+');
    if (next != -1) {
      reader.unread(next);
      builder.deleteCharAt(builder.length() - 1);
    }

    String result = builder.toString();
    if (!JSONNumber.isJSONCompilant(result)) {
      throw new JSONException("String not in JSON number format: " + result);
    }

    return new JSONNumber(result);
  }

  private JSONObject scanObject(PushbackReader reader) throws IOException,
      JSONException {
    JSONObject object = new JSONObject();
    boolean commavalid = false;

    while (true) {
      JSONString key;

      switch (scanCharacter(reader)) {
        case '}':
          return object;
        case ',':
          if (!commavalid) {
            throw new JSONException("Comma not expected here");
          } else if (scanCharacter(reader) != '"') {
            throw new JSONException("Expected an identifier");
          } // do not break, scan an object entry.
        case '"':
          key = scanString(reader);
          if (scanCharacter(reader) != ':') {
            throw new JSONException("Expected a colon");
          } else {
            object.put(key, scanValue(reader));
            commavalid = true;
            break;
          }
        default:
          throw new JSONException("Expected an identifier");
      }
    }
  }

  private JSONString scanString(PushbackReader reader) throws IOException,
      JSONException {
    char prev = 0;
    char next = (char) reader.read();
    StringBuilder sb = new StringBuilder();

    while (next != '"' || prev == '\\') {
      sb.append(next);
      if (prev == '\\') {
        prev = 0;
      } else {
        prev = next;
      }
      next = (char) reader.read();
    }

    return new JSONString(sb.toString());
  }

  private char scanCharacter(PushbackReader reader) throws IOException {
    char next;

    do {
      next = (char) reader.read();
    } while (Character.isWhitespace(next));

    return next;
  }
}
